﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace WaterExample
{
    /// <summary>
    /// Initialize a new instance of GaussianBlur.
    /// Subclass of PostProcessor
    /// </summary>
    class GaussianBlur : PostProcessor
    {
        #region variables

        readonly float _blurAmount;

        readonly float[] _weightsH;
        readonly float[] _weightsV;
        readonly Vector2[] _offsetsH;
        readonly Vector2[] _offsetsV;

        #endregion

        /// <summary>
        /// Initialize a new instance of this class. 
        /// Blurs a given texture.
        /// </summary>
        /// <param name="graphics">GraphicsDevice of the Game</param>
        /// <param name="content">Have to be a RessourceContentManager with the resource Resource.ResourceManager </param>
        /// <param name="blurAmount">Value to regular the strength of the blur effect</param>
        /// <param name="texture">Texture which should be blured</param>
        /// <param name="globalWidth">Width of the RenderTarget</param>
        /// <param name="globalHeight">Height of the RenderTarget</param>
        public GaussianBlur(GraphicsDevice graphics, ContentManager content, float blurAmount, Texture2D texture, int globalWidth, int globalHeight)
            : base(content.Load<Effect>("Shaders/GaussianBlur"), graphics, globalWidth, globalHeight)
        {
            _blurAmount = blurAmount;
            Input = texture;

            //Calculate weight/offsets for horizontal pass
            CalcSettings(1.0f / graphics.Viewport.Width, 0, out _weightsH, out _offsetsH);

            //Calculate weight/offsets for vertical pass
            CalcSettings(0, 1.0f / graphics.Viewport.Height, out _weightsV, out _offsetsV);

            OutputRenderTarget = new RenderTarget2D(GraphicsDevice, globalWidth, globalHeight, false, SurfaceFormat.Vector4, DepthFormat.None);
            TempRenderTarget = new RenderTarget2D(GraphicsDevice, globalWidth, globalHeight, false, SurfaceFormat.Vector4, DepthFormat.None);
        }
      

        /// <summary>
        /// Calculates weights and offsets for GaussianBlur effect
        /// </summary>
        /// <param name="w">Width between [0,1]</param>
        /// <param name="h">Height between [0,1]</param>
        /// <param name="weights">Returns Weights</param>
        /// <param name="offsets">Returns Offsets</param>
        private void CalcSettings(float w, float h, out float[] weights, out Vector2[] offsets)
        {
            //15 Samples
            weights = new float[15];
            offsets = new Vector2[15];

            // Calculate values for center pixel
            weights[0] = GaussianFn(0);
            offsets[0] = new Vector2(0, 0);

            float total = weights[0];

            //Calculate sampels in pairs
            for (int i = 0; i < 7; i++)
            {
                //Weight each pair of samples according to Gaussian function
                float weight = GaussianFn(i + 1);
                weights[i * 2 + 1] = weight;
                weights[i * 2 + 2] = weight;
                total += weight * 2;

                //Samples are offset by 1.5 pixel, to make use of filtering halfway between pixels
                float offset = i * 2 + 1.5f;
                Vector2 offsetVec = new Vector2(w, h) * offset;
                offsets[i * 2 + 1] = offsetVec;
                offsets[i * 2 + 2] = -offsetVec;
            }
            //Divide all weights by total so they will add up to 1
            for (int i = 0; i < weights.Length; i++)
                weights[i] /= total;
        }

        float GaussianFn(float x)
        {
            return (float)((1.0f / Math.Sqrt(2 * Math.PI * _blurAmount * _blurAmount)) * Math.Exp(-(x * x) / (2 * _blurAmount * _blurAmount)));
        }

        /// <summary>
        /// Draw the input texture and blurs it.
        /// </summary>
        /// <param name="depthTexture">Texture which should be blured.</param>
        /// <returns>Returns the resulting scene as renderTarget</returns>
        public override RenderTarget2D Draw(Texture2D depthTexture)
        {
            Input = depthTexture;
            CreateDepthTarget();

            //Set values for horizontal pass
            Effect.Parameters["Offsets"].SetValue(_offsetsH);
            Effect.Parameters["Weights"].SetValue(_weightsH);
            Effect.Parameters["Texture"].SetValue(OutputRenderTarget);

            //Render this pass into the RenderCapture
            Blur(TempRenderTarget, OutputRenderTarget);

            //Set values for vertical pass
            Effect.Parameters["Offsets"].SetValue(_offsetsV);
            Effect.Parameters["Weights"].SetValue(_weightsV);
            Effect.Parameters["Texture"].SetValue(TempRenderTarget);

            //Render the final pass
            Blur(OutputRenderTarget, TempRenderTarget);

            return OutputRenderTarget;
        }

        //Blurs the incoming rendertarget and returns the blured once
        void Blur(RenderTarget2D to, RenderTarget2D from)
        {
            GraphicsDevice.SetRenderTarget(to);

            GraphicsDevice.Clear(Color.Black);

            SpriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);

            Effect.CurrentTechnique.Passes[0].Apply();

            SpriteBatch.Draw(from, new Rectangle(0, 0, GlobalWidth, GlobalHeight), Color.White);

            SpriteBatch.End();

            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            GraphicsDevice.SetRenderTarget(null);
        }

        //Converts incoming texture into a rendertarget
        void CreateDepthTarget()
        {
            GraphicsDevice.SetRenderTarget(OutputRenderTarget);

            GraphicsDevice.Clear(Color.Black);

            SpriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);

            SpriteBatch.Draw(Input, new Rectangle(0, 0, GlobalWidth, GlobalHeight), Color.White);

            SpriteBatch.End();

            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            GraphicsDevice.SetRenderTarget(null);
        }
    }
}
